/*
 * Copyright 2020-2025 the original author or authors.
 * You cannot use this file unless authorized by the author.
 */

package org.ipig.commons.helper;

import com.alibaba.fastjson.JSONObject;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Aggregates;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.ipig.constants.DbmsOperators;
import org.ipig.constants.MongoCnst;
import org.ipig.constants.SymbolCnst;

import java.util.ArrayList;
import java.util.List;

/**
 * MongoQLHelper
 *
 * @author <a href="mailto:comchnts@163.com">chinats</a>
 * @version $$Id: MongoQLHelper.java 22 2020-05-20 03:47:46Z ts <comchnts@163.com> $$
 * @since 1.0
 */
@Slf4j
public class MongoQLHelper {
    public static String REGEX_MONGO_SQL_COLLECTION_NAME = "getCollection\\('(\\w+?)'\\)";
    //    public static String MONGO_SQL_COLUMN_NAME = "'(\\w+?)':";
    //    public static String MONGO_FUNCTION_FIND = "\\.find\\((.*?)\\)";
    public static String MONGO_FUNCTION_FIND = SymbolCnst.DOT + "find" + SymbolCnst.LEFT_PARENTHESIS;
    //.limit(
    public static String MONGO_FUNCTION_LIMIT = SymbolCnst.DOT + "limit" + SymbolCnst.LEFT_PARENTHESIS;
    //".sort("
    public static String MONGO_FUNCTION_SORT = SymbolCnst.DOT + "sort" + SymbolCnst.LEFT_PARENTHESIS;

    /**
     * find
     *
     * @param mongoConn MongoDatabase
     * @param mql       String
     * @return JSONArray
     */
    public static List<Document> find(MongoDatabase mongoConn, final String mql) {
        return find(mongoConn, mql, 2000);
    }

    /**
     * find
     *
     * @param mongoConn MongoDatabase
     * @param mql       String
     * @param batchSize int
     * @return JSONArray
     */
    public static List<Document> find(MongoDatabase mongoConn, final String mql, final int batchSize) {
        List<Document> lists = new ArrayList();
        MongoCursor<Document> cursor = null;
        try {
            String collectionName = getCollectionName(mql);
            List<Bson> pipeline = buildPipelineOperators(mql);
            cursor = mongoConn.getCollection(collectionName).aggregate(pipeline).batchSize(batchSize).allowDiskUse(true).iterator();
            while (cursor.hasNext()) {
                lists.add(cursor.next());
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            log.error(ExceptionUtils.getFullStackTrace(ex));
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return lists;
    }


    /**
     * findOne
     *
     * @param mongoConn
     * @param mql
     * @return
     */
    public static Document findOne(MongoDatabase mongoConn, String mql) {
        Document doc = null;
        MongoCursor<Document> cursor = null;
        try {
            cursor = findToCursor(mongoConn, mql, 1);
            if (cursor != null && cursor.hasNext()) {
                doc = cursor.next();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return doc;
    }

    /**
     * findToCursor
     * 特别提醒：使用完成之后必须要Cursor
     *
     * @param mongoConn
     * @param mql
     * @return
     */
    public static MongoCursor<Document> findToCursor(MongoDatabase mongoConn, final String mql) {
        return findToCursor(mongoConn, mql, 2000);
    }


    /**
     * findToCursor
     * 特别提醒：使用完成之后必须要Cursor
     *
     * @param mongoConn
     * @param mql
     * @param batchSize
     * @return
     */
    public static MongoCursor<Document> findToCursor(MongoDatabase mongoConn, final String mql, final int batchSize) {
        String collectionName = getCollectionName(mql);
        MongoCollection<Document> coll = mongoConn.getCollection(collectionName);
        FindIterable<Document> findIterable = null;
        Document matchDoc = getMatchDoc(mql);
        if (matchDoc != null && !matchDoc.isEmpty()) {
            findIterable = coll.find(matchDoc);
        } else {
            findIterable = coll.find();
        }
        Document projectDoc = getProjectDoc(mql);
        if (projectDoc != null && !projectDoc.isEmpty()) {
            findIterable = findIterable.projection(projectDoc);
        }
        Document sortDoc = getSortDoc(mql);
        if (sortDoc != null && !sortDoc.isEmpty()) {
            findIterable = findIterable.sort(sortDoc);
        }
        int limit = getLimit(mql);
        if (limit > 0) {
            findIterable = findIterable.limit(limit);
        }
        MongoCursor<Document> cursor = findIterable.iterator();
        return cursor;
    }


    /**
     * buildPipelineOperators
     *
     * @param mql String
     * @return List<Bson>
     */
    public static List<Bson> buildPipelineOperators(final String mql) {
        Bson matchOperators = getMatchOperators(mql);
        Bson projectOperators = getProjectOperators(mql);
        Bson sortOperators = getSortOperators(mql);
        Bson limitOperators = getLimitOperators(mql);
        List<Bson> pipeline = new ArrayList<>();
        if (matchOperators != null) {
            pipeline.add(matchOperators);
        }
        if (projectOperators != null) {
            pipeline.add(projectOperators);
        }
        if (sortOperators != null) {
            pipeline.add(sortOperators);
        }
        if (limitOperators != null) {
            pipeline.add(limitOperators);
        }
        log.debug(pipeline.toString());
        return pipeline;
    }


    /**
     * getCollectionName
     *
     * @param mql
     * @return
     */
    public static String getCollectionName(final String mql) {
        return RegexHelper.getWithRegExp(mql, REGEX_MONGO_SQL_COLLECTION_NAME);
    }


    /**
     * getMatchDoc
     *
     * @param mql
     * @return
     */
    public static Document getMatchDoc(final String mql) {
        Document matchDoc = null;
        //.find({'period':{'$gte':2019}},{'periodMonth':1,'periodYear':1,'remark':1})
        int startPos = StringUtils.indexOf(mql, MONGO_FUNCTION_FIND);
        if (startPos > 0) {
            int start = StringUtils.indexOf(mql, SymbolCnst.LEFT_BRACES, startPos);
            int end = 0, matchPos = 0;

            int projectPos = StringUtils.indexOf(mql, "},{", start);
            if (projectPos > 0) {
                end = projectPos + 1;
            } else if ((matchPos = StringUtils.indexOf(mql, "}}", start)) > 0) {
                end = matchPos + 2;
            } else {
                end = StringUtils.indexOf(mql, SymbolCnst.RIGHT_BRACES, start) + 1;
            }
            String matchText = StringUtils.substring(mql, start, end);
            matchDoc = JSONObject.parseObject(matchText, Document.class);
        }
        return matchDoc;
    }

    /**
     * getMatchOperators
     *
     * @param mql
     * @return
     */
    public static Bson getMatchOperators(final String mql) {
        Bson docOperators = null;
        Document matchDoc = getMatchDoc(mql);
        if (matchDoc != null && !matchDoc.isEmpty()) {
            docOperators = Aggregates.match(matchDoc);
        }
        return docOperators;
    }

    /**
     * getProjectDoc
     *
     * @param mql
     * @return
     */
    public static Document getProjectDoc(final String mql) {
        Document projectDoc = new Document();
        //.find({'period':{'$gte':2019}},{'period.periodMonth':1})
        int startPos = StringUtils.indexOf(mql, MONGO_FUNCTION_FIND);
        if (startPos > 0) {
            int start = StringUtils.indexOf(mql, "},{", startPos);
            if (start > 0) {
                int end = StringUtils.indexOf(mql, "})", start);
                String projectText = StringUtils.substring(mql, start + 2, end + 1);
                projectDoc = JSONObject.parseObject(projectText, Document.class);
            }
        }
        return projectDoc;
    }

    /**
     * getProjectOperators
     *
     * @param mql
     * @return
     */
    public static Bson getProjectOperators(final String mql) {
        Bson docOperators = null;
        Document projectDoc = getProjectDoc(mql);
        if (projectDoc != null && !projectDoc.isEmpty()) {
            docOperators = Aggregates.project(projectDoc);
        }
        return docOperators;
    }


    /**
     * getSortDoc
     *
     * @param mql
     * @return
     */
    public static Document getSortDoc(final String mql) {
        Document sortDoc = new Document();
        int startPos = StringUtils.indexOf(mql, MONGO_FUNCTION_SORT);
        if (startPos > 0) {
            int start = StringUtils.indexOf(mql, SymbolCnst.LEFT_BRACES, startPos);
            int end = StringUtils.indexOf(mql, SymbolCnst.RIGHT_BRACES, start);
            String sortText = StringUtils.substring(mql, start, end + 1);
            sortDoc = JSONObject.parseObject(sortText, Document.class);
        } else {
            sortDoc.append(MongoCnst.DEFAULT_ID, DbmsOperators.SortType.DESC.num);
        }
        return sortDoc;
    }

    /**
     * getSortDoc
     *
     * @param mql
     * @return
     */
    public static Bson getSortOperators(final String mql) {
        Bson docOperators = null;
        Document sortDoc = getSortDoc(mql);
        if (sortDoc != null && !sortDoc.isEmpty()) {
            docOperators = Aggregates.sort(sortDoc);
        }
        return docOperators;
    }

    /**
     * getLimit
     *
     * @param mql
     * @return
     */
    public static int getLimit(final String mql) {
        //.limit(5)
        int startPos = StringUtils.indexOf(mql, MONGO_FUNCTION_LIMIT);
        int limit = 0;
        if (startPos > 0) {
            int start = StringUtils.indexOf(mql, SymbolCnst.LEFT_PARENTHESIS, startPos) + 1;
            int end = StringUtils.indexOf(mql, SymbolCnst.RIGHT_PARENTHESIS, start);
            String limitText = StringUtils.substring(mql, start, end);
            limitText = StringUtils.trimToEmpty(limitText);
            limit = NumberHelper.toInt(limitText, 0);
        }
        return limit;
    }

    /**
     * getLimitOperators
     *
     * @param mql
     * @return
     */
    public static Bson getLimitOperators(final String mql) {
        Bson docOperators = null;
        int limit = getLimit(mql);
        if (limit > 0) {
            docOperators = Aggregates.limit(limit);
        }
        return docOperators;
    }
}